Micron Document
C.S.Burner 🪙 ━►🔥GIT Node

Node / reticulum_mirror / MeshChatX / files / meshchatx / src / frontend / components / relay / RelayHostModerationPage.vue

Displaying Raw • Download

meshchatx/src/frontend/components/relay/RelayHostModerationPage.vue 65ca9ddc823fcc2c8f8653c553b944b85db0875f (65ca9ddc) Text, 41.56 KB

T8b949e<!-- SPDX-License-Identifier: 0BSD -->

Tff7b72<templateTff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex min-h-0 flex-1 flex-col overflow-hidden bg-sem-canvas text-sem-fgTa5d6ff"Tff7b72>
Tff7b72<div Te6edf3:class=Ta5d6ff"Ta5d6ffRELAY_HOST_PAGE_HEADERTa5d6ff"Tff7b72>
Tff7b72<button Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff" Te6edf3:class=Ta5d6ff"Ta5d6ffRELAY_HOST_ICON_BTNTa5d6ff" Te6edf3:title=Ta5d6ff"Te6edf3$tTa5d6ff(Ta5d6ff'Ta5d6ffrelay_chat.backTa5d6ff'Ta5d6ff)Ta5d6ff" @Te6edf3click=Ta5d6ff"Te6edf3$emitTa5d6ff(Ta5d6ff'Ta5d6ffbackTa5d6ff'Ta5d6ff)Ta5d6ff"Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffarrow-leftTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-5Ta5d6ff" Tff7b72/>
Tff7b72</button>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffmin-w-0 flex-1Ta5d6ff"Tff7b72>
Tff7b72<h2 Te6edf3class=Ta5d6ff"Ta5d6fftruncate text-lg font-semibold text-sem-fgTa5d6ff"Tff7b72>{{ pageTitle }}Tff7b72</h2>
Tff7b72<p Te6edf3v-if=Ta5d6ff"Ta5d6ffhubTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffmt-0.5 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-xs text-sem-fg-mutedTa5d6ff"Tff7b72>
Tff7b72<span Te6edf3class=Ta5d6ff"Ta5d6ffinline-flex items-center gap-1.5Ta5d6ff"Tff7b72>
Tff7b72<span
Te6edf3class=Ta5d6ff"Ta5d6ffinline-block size-2 shrink-0 rounded-fullTa5d6ff"
Te6edf3:class=Ta5d6ff"Ta5d6ffhub.running ? Ta5d6ff'Ta5d6ffbg-sem-successTa5d6ff'Ta5d6ff : Ta5d6ff'Ta5d6ffbg-sem-fg-mutedTa5d6ff'Ta5d6ff"
Tff7b72>Tff7b72</span>
{{ hub.running ? Te6edf3$t("relay_chat.host_status_running") : Te6edf3$t("relay_chat.host_status_stopped") }}
Tff7b72</span>
Tff7b72<template Te6edf3v-if=Ta5d6ff"Ta5d6ffhub.running && liveUptimeSeconds > 0Ta5d6ff"Tff7b72>
Tff7b72<span Te6edf3class=Ta5d6ff"Ta5d6fftext-sem-border-strongTa5d6ff" Te6edf3aria-hidden=Ta5d6ff"Ta5d6fftrueTa5d6ff"Tff7b72>·Tff7b72</span>
Tff7b72<spanTff7b72>{{
Te6edf3$t("relay_chat.host_moderation_uptime", { time: formatUptime(liveUptimeSeconds) })
}}Tff7b72</span>
Tff7b72</template>
Tff7b72<template Te6edf3v-if=Ta5d6ff"Ta5d6ffnavbarMemberCount != nullTa5d6ff"Tff7b72>
Tff7b72<span Te6edf3class=Ta5d6ff"Ta5d6fftext-sem-border-strongTa5d6ff" Te6edf3aria-hidden=Ta5d6ff"Ta5d6fftrueTa5d6ff"Tff7b72>·Tff7b72</span>
Tff7b72<spanTff7b72>{{ Te6edf3$t("relay_chat.host_moderation_members", { count: navbarMemberCount }) }}Tff7b72</span>
Tff7b72</template>
Tff7b72</p>
Tff7b72</div>
Tff7b72</div>

Tff7b72<div Te6edf3:class=Ta5d6ff"Ta5d6ffRELAY_HOST_PAGE_TABSTa5d6ff" Te6edf3role=Ta5d6ff"Ta5d6fftablistTa5d6ff"Tff7b72>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3role=Ta5d6ff"Ta5d6fftabTa5d6ff"
Te6edf3:aria-selected=Ta5d6ff"Ta5d6fftab === Ta5d6ff'Ta5d6ffroomsTa5d6ff'Ta5d6ff"
Te6edf3:class=Ta5d6ff"Ta5d6ff[RELAY_HOST_PAGE_TAB, tab === Ta5d6ff'Ta5d6ffroomsTa5d6ff'Ta5d6ff ? RELAY_HOST_PAGE_TAB_ACTIVE : RELAY_HOST_PAGE_TAB_IDLE]Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffsetTab(Ta5d6ff'Ta5d6ffroomsTa5d6ff'Ta5d6ff)Ta5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffpoundTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-4Ta5d6ff" Tff7b72/>
{{ Te6edf3$t("relay_chat.host_moderation_tab_rooms") }}
Tff7b72</button>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3role=Ta5d6ff"Ta5d6fftabTa5d6ff"
Te6edf3:aria-selected=Ta5d6ff"Ta5d6fftab === Ta5d6ff'Ta5d6ffmembersTa5d6ff'Ta5d6ff"
Te6edf3:class=Ta5d6ff"Ta5d6ff[
RELAY_HOST_PAGE_TAB,
tab === Ta5d6ff'Ta5d6ffmembersTa5d6ff'Ta5d6ff ? RELAY_HOST_PAGE_TAB_ACTIVE : RELAY_HOST_PAGE_TAB_IDLE,
]Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffsetTab(Ta5d6ff'Ta5d6ffmembersTa5d6ff'Ta5d6ff)Ta5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffaccount-groupTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-4Ta5d6ff" Tff7b72/>
{{ Te6edf3$t("relay_chat.host_moderation_tab_members") }}
Tff7b72</button>
Tff7b72</div>

Tff7b72<div Te6edf3v-if=Ta5d6ff"Ta5d6ff!hubTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffflex flex-1 items-center justify-center p-6 text-sm text-sem-fg-mutedTa5d6ff"Tff7b72>
{{ Te6edf3$t("relay_chat.host_moderation_hub_missing") }}
Tff7b72</div>

Tff7b72<div Te6edf3v-else-if=Ta5d6ff"Ta5d6fftab === Ta5d6ff'Ta5d6ffroomsTa5d6ff'Ta5d6ff" Te6edf3:class=Ta5d6ff"Ta5d6ffRELAY_HOST_PAGE_BODYTa5d6ff"Tff7b72>
Tff7b72<div Te6edf3:class=Ta5d6ff"Ta5d6ff[RELAY_HOST_PAGE_LIST, isNarrow && selectedRoom ? Ta5d6ff'Ta5d6ffhiddenTa5d6ff'Ta5d6ff : Ta5d6ff'Ta5d6ffflex flex-1 lg:flex-noneTa5d6ff'Ta5d6ff]Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffshrink-0 space-y-3 border-b border-sem-border p-3 sm:p-4Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffrelativeTa5d6ff"Tff7b72>
Tff7b72<MaterialDesignIcon
Te6edf3icon-name=Ta5d6ff"Ta5d6ffmagnifyTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffpointer-events-none absolute left-2.5 top-1/2 z-10 size-4 -translate-y-1/2 text-sem-fg-mutedTa5d6ff"
Tff7b72/>
Tff7b72<input
Te6edf3v-model=Ta5d6ff"Ta5d6ffroomsSearchTa5d6ff"
Te6edf3type=Ta5d6ff"Ta5d6ffsearchTa5d6ff"
Te6edf3:placeholder=Ta5d6ff"Te6edf3$tTa5d6ff(Ta5d6ff'Ta5d6ffrelay_chat.host_rooms_searchTa5d6ff'Ta5d6ff)Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffinput-field !py-2 !pl-9 !pr-3Ta5d6ff"
Tff7b72/>
Tff7b72</div>
Tff7b72<button
Te6edf3v-if=Ta5d6ff"Ta5d6ff!showAddRoomFormTa5d6ff"
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffflex w-full items-center gap-2 rounded-xl border-2 border-dashed border-sem-border px-3 py-3 text-left text-sm text-sem-fg-muted transition-colors hover:border-sem-accent hover:bg-sem-surface/40 hover:text-sem-accentTa5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffshowAddRoomForm = trueTa5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffplus-circle-outlineTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-5 shrink-0Ta5d6ff" Tff7b72/>
Tff7b72<span Te6edf3class=Ta5d6ff"Ta5d6fffont-mediumTa5d6ff"Tff7b72>{{ Te6edf3$t("relay_chat.host_add_room") }}Tff7b72</span>
Tff7b72</button>
Tff7b72<form
v-else
Te6edf3class=Ta5d6ff"Ta5d6ffspace-y-2.5 rounded-xl border border-sem-border bg-sem-surface-raised/40 p-3Ta5d6ff"
@submit.Te6edf3prevent=Ta5d6ff"Ta5d6ffcreateRoomTa5d6ff"
Tff7b72>
Tff7b72<input
Te6edf3v-model=Ta5d6ff"Ta5d6ffnewRoom.nameTa5d6ff"
Te6edf3type=Ta5d6ff"Ta5d6fftextTa5d6ff"
Te6edf3:placeholder=Ta5d6ff"Te6edf3$tTa5d6ff(Ta5d6ff'Ta5d6ffrelay_chat.host_room_nameTa5d6ff'Ta5d6ff)Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffinput-field w-full !py-2.5 !text-smTa5d6ff"
autofocus
Tff7b72/>
Tff7b72<input
Te6edf3v-model=Ta5d6ff"Ta5d6ffnewRoom.topicTa5d6ff"
Te6edf3type=Ta5d6ff"Ta5d6fftextTa5d6ff"
Te6edf3:placeholder=Ta5d6ff"Te6edf3$tTa5d6ff(Ta5d6ff'Ta5d6ffrelay_chat.host_room_topicTa5d6ff'Ta5d6ff)Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffinput-field w-full !py-2.5 !text-smTa5d6ff"
Tff7b72/>
Tff7b72<input
Te6edf3v-model=Ta5d6ff"Ta5d6ffnewRoom.keyTa5d6ff"
Te6edf3type=Ta5d6ff"Ta5d6ffpasswordTa5d6ff"
Te6edf3:placeholder=Ta5d6ff"Te6edf3$tTa5d6ff(Ta5d6ff'Ta5d6ffrelay_chat.host_room_key_placeholderTa5d6ff'Ta5d6ff)Ta5d6ff"
Te6edf3autocomplete=Ta5d6ff"Ta5d6ffoffTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffinput-field w-full !py-2.5 !text-smTa5d6ff"
Tff7b72/>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex gap-2 pt-0.5Ta5d6ff"Tff7b72>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffsubmitTa5d6ff"
Te6edf3:class=Ta5d6ff"Ta5d6ff[btnPrimary, Ta5d6ff'Ta5d6ffflex-1 !py-2.5 !text-smTa5d6ff'Ta5d6ff]Ta5d6ff"
Te6edf3:disabled=Ta5d6ff"Ta5d6ffcreatingRoomTa5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffplusTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-4Ta5d6ff" Tff7b72/>
{{ Te6edf3$t("relay_chat.host_add_room") }}
Tff7b72</button>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3:class=Ta5d6ff"Ta5d6ff[btnSecondary, Ta5d6ff'Ta5d6ff!py-2.5 !text-smTa5d6ff'Ta5d6ff]Ta5d6ff"
Te6edf3:disabled=Ta5d6ff"Ta5d6ffcreatingRoomTa5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffcancelAddRoomTa5d6ff"
Tff7b72>
{{ Te6edf3$t("common.cancel") }}
Tff7b72</button>
Tff7b72</div>
Tff7b72</form>
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffmin-h-0 flex-1 overflow-y-auto custom-scrollbar p-3 sm:p-4Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3v-if=Ta5d6ff"Ta5d6ffroomsLoadingTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffpy-12 text-center text-sm text-sem-fg-mutedTa5d6ff"Tff7b72>
{{ Te6edf3$t("common.loading") }}
Tff7b72</div>
Tff7b72<div Te6edf3v-else-if=Ta5d6ff"Ta5d6fffilteredRooms.length === 0Ta5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffpy-12 text-center text-sm text-sem-fg-mutedTa5d6ff"Tff7b72>
{{
roomsSearch.trim()
? Te6edf3$t("relay_chat.host_rooms_search_empty")
: Te6edf3$t("relay_chat.host_no_rooms")
}}
Tff7b72</div>
Tff7b72<ul v-else Te6edf3class=Ta5d6ff"Ta5d6ffspace-y-1.5Ta5d6ff"Tff7b72>
Tff7b72<li
Te6edf3v-for=Ta5d6ff"Ta5d6ffroom in filteredRoomsTa5d6ff"
Te6edf3:key=Ta5d6ff"Ta5d6ffroom.nameTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffcursor-pointer px-3 py-2.5Ta5d6ff"
Te6edf3:class=Ta5d6ff"Ta5d6ff[
RELAY_HOST_LIST_ITEM,
selectedRoom === room.name ? RELAY_HOST_LIST_ITEM_SELECTED : RELAY_HOST_LIST_ITEM_IDLE,
]Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffselectedRoom = room.nameTa5d6ff"
Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex items-start justify-between gap-2Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffmin-w-0Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex items-center gap-1.5 font-medium text-sem-fgTa5d6ff"Tff7b72>
Tff7b72<spanTff7b72>#{{ room.name }}Tff7b72</span>
Tff7b72<span
Te6edf3v-if=Ta5d6ff"Ta5d6ffroom.has_keyTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffinline-flexTa5d6ff"
Te6edf3:title=Ta5d6ff"Te6edf3$tTa5d6ff(Ta5d6ff'Ta5d6ffrelay_chat.host_room_keyedTa5d6ff'Ta5d6ff)Ta5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon
Te6edf3icon-name=Ta5d6ff"Ta5d6fflockTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffsize-3.5 shrink-0 text-sem-fg-mutedTa5d6ff"
Tff7b72/>
Tff7b72</span>
Tff7b72</div>
Tff7b72<div Te6edf3v-if=Ta5d6ff"Ta5d6ffroom.topicTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6fftruncate text-xs text-sem-fg-mutedTa5d6ff"Tff7b72>
{{ room.topic }}
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffmt-1 flex flex-wrap gap-x-3 text-xs text-sem-fg-mutedTa5d6ff"Tff7b72>
Tff7b72<spanTff7b72>{{ room.members }} {{ Te6edf3$t("relay_chat.host_clients") }}Tff7b72</span>
Tff7b72<spanTff7b72>{{ room.message_count || 0 }} msgsTff7b72</span>
Tff7b72</div>
Tff7b72</div>
Tff7b72<button
Te6edf3v-if=Ta5d6ff"Ta5d6ffroom.registeredTa5d6ff"
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffshrink-0 rounded-lg p-1.5 text-sem-fg-muted hover:text-sem-dangerTa5d6ff"
Te6edf3:title=Ta5d6ff"Te6edf3$tTa5d6ff(Ta5d6ff'Ta5d6ffrelay_chat.host_delete_roomTa5d6ff'Ta5d6ff)Ta5d6ff"
@click.Te6edf3stop=Ta5d6ff"Ta5d6ffdeleteRoom(room.name)Ta5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6fftrash-can-outlineTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-4Ta5d6ff" Tff7b72/>
Tff7b72</button>
Tff7b72</div>
Tff7b72</li>
Tff7b72</ul>
Tff7b72</div>
Tff7b72</div>

Tff7b72<div Te6edf3:class=Ta5d6ff"Ta5d6ff[RELAY_HOST_PAGE_DETAIL, isNarrow && !selectedRoom ? Ta5d6ff'Ta5d6ffhiddenTa5d6ff'Ta5d6ff : Ta5d6ff'Ta5d6ffflexTa5d6ff'Ta5d6ff]Ta5d6ff"Tff7b72>
Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6ffisNarrow && selectedRoomTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffflex shrink-0 items-center gap-2 border-b border-sem-border px-3 py-2 lg:hiddenTa5d6ff"
Tff7b72>
Tff7b72<button Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff" Te6edf3:class=Ta5d6ff"Ta5d6ffRELAY_HOST_ICON_BTNTa5d6ff" @Te6edf3click=Ta5d6ff"Ta5d6ffselectedRoom = nullTa5d6ff"Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffarrow-leftTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-5Ta5d6ff" Tff7b72/>
Tff7b72</button>
Tff7b72<span Te6edf3class=Ta5d6ff"Ta5d6fffont-semibold text-sem-fgTa5d6ff"Tff7b72>#{{ selectedRoom }}Tff7b72</span>
Tff7b72</div>
Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6ff!selectedRoomTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffflex flex-1 flex-col items-center justify-center gap-2 p-6 text-center text-sm text-sem-fg-mutedTa5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffpoundTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-10 opacity-40Ta5d6ff" Tff7b72/>
{{ Te6edf3$t("relay_chat.host_rooms_select") }}
Tff7b72</div>
Tff7b72<template v-elseTff7b72>
Tff7b72<div Te6edf3:class=Ta5d6ff"Ta5d6ff[RELAY_HOST_DETAIL_HEADER, Ta5d6ff'Ta5d6ffhidden lg:blockTa5d6ff'Ta5d6ff]Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fffont-semibold text-sem-fgTa5d6ff"Tff7b72>#{{ selectedRoom }}Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fftext-xs text-sem-fg-mutedTa5d6ff"Tff7b72>{{ Te6edf3$t("relay_chat.host_room_activity") }}Tff7b72</div>
Tff7b72</div>
Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6ffselectedRoomObjTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffflex flex-wrap items-center gap-2 border-b border-sem-border px-3 py-2 text-xs text-sem-fg-mutedTa5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon
Te6edf3:icon-name=Ta5d6ff"Ta5d6ffselectedRoomObj.has_key ? Ta5d6ff'Ta5d6fflockTa5d6ff'Ta5d6ff : Ta5d6ff'Ta5d6fflock-open-variantTa5d6ff'Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffsize-4 shrink-0Ta5d6ff"
Tff7b72/>
Tff7b72<span Te6edf3class=Ta5d6ff"Ta5d6ffflex-1Ta5d6ff"Tff7b72>
{{
selectedRoomObj.has_key
? Te6edf3$t("relay_chat.host_room_keyed")
: Te6edf3$t("relay_chat.host_room_unkeyed")
}}
Tff7b72</span>
Tff7b72<button Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6fffont-medium text-sem-accent hover:underlineTa5d6ff" @Te6edf3click=Ta5d6ff"Ta5d6ffsetRoomKeyTa5d6ff"Tff7b72>
{{
selectedRoomObj.has_key
? Te6edf3$t("relay_chat.host_change_room_key")
: Te6edf3$t("relay_chat.host_set_room_key")
}}
Tff7b72</button>
Tff7b72<button
Te6edf3v-if=Ta5d6ff"Ta5d6ffselectedRoomObj.has_keyTa5d6ff"
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6fffont-medium text-sem-danger hover:underlineTa5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffclearRoomKeyTa5d6ff"
Tff7b72>
{{ Te6edf3$t("relay_chat.host_clear_room_key") }}
Tff7b72</button>
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffmin-h-0 flex-1 overflow-y-auto custom-scrollbar p-3 sm:p-4Ta5d6ff"Tff7b72>
Tff7b72<ul Te6edf3v-if=Ta5d6ff"Ta5d6ffroomMessages.length > 0Ta5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffspace-y-2Ta5d6ff"Tff7b72>
Tff7b72<li Te6edf3v-for=Ta5d6ff"Ta5d6ff(msg, idx) in roomMessagesTa5d6ff" Te6edf3:key=Ta5d6ff"Ta5d6ffidxTa5d6ff" Te6edf3:class=Ta5d6ff"Ta5d6ffRELAY_HOST_MESSAGETa5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex flex-wrap items-center gap-x-2 text-xs text-sem-fg-mutedTa5d6ff"Tff7b72>
Tff7b72<span Te6edf3:style=Ta5d6ff"Ta5d6ff{ color: colorForHash(msg.peer) }Ta5d6ff"Tff7b72>{{
msg.nick || formatHash(msg.peer)
}}Tff7b72</span>
Tff7b72<spanTff7b72>{{ formatTime(msg.ts) }}Tff7b72</span>
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffmt-1 whitespace-pre-wrap break-wordsTa5d6ff"Tff7b72>{{ msg.text }}Tff7b72</div>
Tff7b72</li>
Tff7b72</ul>
Tff7b72<div v-else Te6edf3class=Ta5d6ff"Ta5d6ffpy-8 text-center text-sm text-sem-fg-mutedTa5d6ff"Tff7b72>
{{ Te6edf3$t("relay_chat.host_no_activity") }}
Tff7b72</div>
Tff7b72</div>
Tff7b72</template>
Tff7b72</div>
Tff7b72</div>

Tff7b72<div v-else Te6edf3:class=Ta5d6ff"Ta5d6ffRELAY_HOST_PAGE_BODYTa5d6ff"Tff7b72>
Tff7b72<div Te6edf3:class=Ta5d6ff"Ta5d6ff[RELAY_HOST_PAGE_LIST, isNarrow && selectedMember ? Ta5d6ff'Ta5d6ffhiddenTa5d6ff'Ta5d6ff : Ta5d6ff'Ta5d6ffflex flex-1 lg:flex-noneTa5d6ff'Ta5d6ff]Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffshrink-0 border-b border-sem-border p-3 sm:p-4Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffrelativeTa5d6ff"Tff7b72>
Tff7b72<MaterialDesignIcon
Te6edf3icon-name=Ta5d6ff"Ta5d6ffmagnifyTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffpointer-events-none absolute left-2.5 top-1/2 z-10 size-4 -translate-y-1/2 text-sem-fg-mutedTa5d6ff"
Tff7b72/>
Tff7b72<input
Te6edf3v-model=Ta5d6ff"Ta5d6ffmembersSearchTa5d6ff"
Te6edf3type=Ta5d6ff"Ta5d6ffsearchTa5d6ff"
Te6edf3:placeholder=Ta5d6ff"Te6edf3$tTa5d6ff(Ta5d6ff'Ta5d6ffrelay_chat.host_members_searchTa5d6ff'Ta5d6ff)Ta5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffinput-field !py-2 !pl-9 !pr-3Ta5d6ff"
Tff7b72/>
Tff7b72</div>
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffmin-h-0 flex-1 overflow-y-auto custom-scrollbar p-3 sm:p-4Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3v-if=Ta5d6ff"Ta5d6ffmembersLoadingTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffpy-12 text-center text-sm text-sem-fg-mutedTa5d6ff"Tff7b72>
{{ Te6edf3$t("common.loading") }}
Tff7b72</div>
Tff7b72<div Te6edf3v-else-if=Ta5d6ff"Ta5d6fffilteredMembers.length === 0Ta5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffpy-12 text-center text-sm text-sem-fg-mutedTa5d6ff"Tff7b72>
{{ Te6edf3$t("relay_chat.no_members") }}
Tff7b72</div>
Tff7b72<ul v-else Te6edf3class=Ta5d6ff"Ta5d6ffspace-y-1.5Ta5d6ff"Tff7b72>
Tff7b72<li
Te6edf3v-for=Ta5d6ff"Ta5d6ffmember in filteredMembersTa5d6ff"
Te6edf3:key=Ta5d6ff"Ta5d6ffmember.hashTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffcursor-pointer px-3 py-2.5Ta5d6ff"
Te6edf3:class=Ta5d6ff"Ta5d6ff[
RELAY_HOST_LIST_ITEM,
selectedMember?.hash === member.hash
? RELAY_HOST_LIST_ITEM_SELECTED
: RELAY_HOST_LIST_ITEM_IDLE,
]Ta5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffselectMember(member)Ta5d6ff"
Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex items-start gap-2Ta5d6ff"Tff7b72>
Tff7b72<span Te6edf3class=Ta5d6ff"Ta5d6ffmt-1.5 size-2 shrink-0 rounded-full bg-sem-successTa5d6ff"Tff7b72>Tff7b72</span>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffmin-w-0 flex-1Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fftruncate font-mediumTa5d6ff" Te6edf3:style=Ta5d6ff"Ta5d6ff{ color: colorForHash(member.hash) }Ta5d6ff"Tff7b72>
{{ member.name }}
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fftruncate font-mono text-xs text-sem-fg-mutedTa5d6ff"Tff7b72>
{{ formatHash(member.hash) }}
Tff7b72</div>
Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6ff!roomFilter && member.rooms?.lengthTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffmt-1 truncate text-xs text-sem-fg-mutedTa5d6ff"
Tff7b72>
{{ member.rooms.map((r) => "#" + r).join(", ") }}
Tff7b72</div>
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex shrink-0 items-center gap-0.5Ta5d6ff"Tff7b72>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffrounded-lg p-1.5 text-sem-fg-muted hover:bg-sem-warning/15 hover:text-sem-warningTa5d6ff"
Te6edf3:title=Ta5d6ff"Te6edf3$tTa5d6ff(Ta5d6ff'Ta5d6ffrelay_chat.ctx_kick_userTa5d6ff'Ta5d6ff)Ta5d6ff"
@click.Te6edf3stop=Ta5d6ff"Ta5d6ffmoderate(member, Ta5d6ff'Ta5d6ffkickTa5d6ff'Ta5d6ff)Ta5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffaccount-removeTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-4Ta5d6ff" Tff7b72/>
Tff7b72</button>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffrounded-lg p-1.5 text-sem-fg-muted hover:bg-sem-danger/15 hover:text-sem-dangerTa5d6ff"
Te6edf3:title=Ta5d6ff"Te6edf3$tTa5d6ff(Ta5d6ff'Ta5d6ffrelay_chat.host_ban_hubTa5d6ff'Ta5d6ff)Ta5d6ff"
@click.Te6edf3stop=Ta5d6ff"Ta5d6ffmoderate(member, roomFilter ? Ta5d6ff'Ta5d6ffroom_banTa5d6ff'Ta5d6ff : Ta5d6ff'Ta5d6ffbanTa5d6ff'Ta5d6ff)Ta5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffblock-helperTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-4Ta5d6ff" Tff7b72/>
Tff7b72</button>
Tff7b72</div>
Tff7b72</div>
Tff7b72</li>
Tff7b72</ul>
Tff7b72</div>
Tff7b72</div>

Tff7b72<div Te6edf3:class=Ta5d6ff"Ta5d6ff[RELAY_HOST_PAGE_DETAIL, isNarrow && !selectedMember ? Ta5d6ff'Ta5d6ffhiddenTa5d6ff'Ta5d6ff : Ta5d6ff'Ta5d6ffflexTa5d6ff'Ta5d6ff]Ta5d6ff"Tff7b72>
Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6ffisNarrow && selectedMemberTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffflex shrink-0 items-center gap-2 border-b border-sem-border px-3 py-2 lg:hiddenTa5d6ff"
Tff7b72>
Tff7b72<button Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff" Te6edf3:class=Ta5d6ff"Ta5d6ffRELAY_HOST_ICON_BTNTa5d6ff" @Te6edf3click=Ta5d6ff"Ta5d6ffselectedMember = nullTa5d6ff"Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffarrow-leftTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-5Ta5d6ff" Tff7b72/>
Tff7b72</button>
Tff7b72<span Te6edf3class=Ta5d6ff"Ta5d6fftruncate font-semiboldTa5d6ff" Te6edf3:style=Ta5d6ff"Ta5d6ff{ color: colorForHash(selectedMember.hash) }Ta5d6ff"Tff7b72>
{{ selectedMember.name }}
Tff7b72</span>
Tff7b72</div>
Tff7b72<div
Te6edf3v-if=Ta5d6ff"Ta5d6ff!selectedMemberTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffflex flex-1 flex-col items-center justify-center gap-2 p-6 text-center text-sm text-sem-fg-mutedTa5d6ff"
Tff7b72>
Tff7b72<MaterialDesignIcon Te6edf3icon-name=Ta5d6ff"Ta5d6ffaccount-searchTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffsize-10 opacity-40Ta5d6ff" Tff7b72/>
{{ Te6edf3$t("relay_chat.host_members_select") }}
Tff7b72</div>
Tff7b72<template v-elseTff7b72>
Tff7b72<div Te6edf3:class=Ta5d6ff"Ta5d6ff[RELAY_HOST_DETAIL_HEADER, Ta5d6ff'Ta5d6ffhidden lg:blockTa5d6ff'Ta5d6ff]Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6fffont-semiboldTa5d6ff" Te6edf3:style=Ta5d6ff"Ta5d6ff{ color: colorForHash(selectedMember.hash) }Ta5d6ff"Tff7b72>
{{ selectedMember.name }}
Tff7b72</div>
Tff7b72<button
Te6edf3type=Ta5d6ff"Ta5d6ffbuttonTa5d6ff"
Te6edf3class=Ta5d6ff"Ta5d6ffmt-0.5 font-mono text-xs text-sem-fg-muted hover:text-sem-accentTa5d6ff"
@Te6edf3click=Ta5d6ff"Ta5d6ffcopyHash(selectedMember.hash)Ta5d6ff"
Tff7b72>
{{ formatHash(selectedMember.hash) }}
Tff7b72</button>
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffmin-h-0 flex-1 overflow-y-auto custom-scrollbar p-3 sm:p-4Ta5d6ff"Tff7b72>
Tff7b72<div Te6edf3v-if=Ta5d6ff"Ta5d6ffmessagesLoadingTa5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffpy-8 text-center text-sm text-sem-fg-mutedTa5d6ff"Tff7b72>
{{ Te6edf3$t("common.loading") }}
Tff7b72</div>
Tff7b72<div Te6edf3v-else-if=Ta5d6ff"Ta5d6ffmemberMessages.length === 0Ta5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffpy-8 text-center text-sm text-sem-fg-mutedTa5d6ff"Tff7b72>
{{ Te6edf3$t("relay_chat.host_no_messages") }}
Tff7b72</div>
Tff7b72<ul v-else Te6edf3class=Ta5d6ff"Ta5d6ffspace-y-2Ta5d6ff"Tff7b72>
Tff7b72<li Te6edf3v-for=Ta5d6ff"Ta5d6ff(msg, idx) in memberMessagesTa5d6ff" Te6edf3:key=Ta5d6ff"Ta5d6ffidxTa5d6ff" Te6edf3:class=Ta5d6ff"Ta5d6ffRELAY_HOST_MESSAGETa5d6ff"Tff7b72>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffflex flex-wrap items-center gap-x-2 text-xs text-sem-fg-mutedTa5d6ff"Tff7b72>
Tff7b72<spanTff7b72>#{{ msg.room }}Tff7b72</span>
Tff7b72<spanTff7b72>{{ formatTime(msg.ts) }}Tff7b72</span>
Tff7b72<span Te6edf3v-if=Ta5d6ff"Ta5d6ffmsg.kind === Ta5d6ff'Ta5d6ffactionTa5d6ff'Ta5d6ff" Te6edf3class=Ta5d6ff"Ta5d6ffitalicTa5d6ff"Tff7b72>actionTff7b72</span>
Tff7b72</div>
Tff7b72<div Te6edf3class=Ta5d6ff"Ta5d6ffmt-1 whitespace-pre-wrap break-wordsTa5d6ff"Tff7b72>{{ msg.text }}Tff7b72</div>
Tff7b72</li>
Tff7b72</ul>
Tff7b72</div>
Tff7b72</template>
Tff7b72</div>
Tff7b72</div>
Tff7b72</div>
Tff7b72</template>

Tff7b72<scriptTff7b72>
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
import DialogUtils from "../../js/DialogUtils";
import ToastUtils from "../../js/ToastUtils";
import {
RELAY_HOST_DETAIL_HEADER,
RELAY_HOST_ICON_BTN,
RELAY_HOST_LIST_ITEM,
RELAY_HOST_LIST_ITEM_IDLE,
RELAY_HOST_LIST_ITEM_SELECTED,
RELAY_HOST_MESSAGE,
RELAY_HOST_PAGE_BODY,
RELAY_HOST_PAGE_DETAIL,
RELAY_HOST_PAGE_HEADER,
RELAY_HOST_PAGE_LIST,
RELAY_HOST_PAGE_TAB,
RELAY_HOST_PAGE_TAB_ACTIVE,
RELAY_HOST_PAGE_TAB_IDLE,
RELAY_HOST_PAGE_TABS,
} from "../../js/relayHostModerationClasses.js";

const BTN_PRIMARY =
"inline-flex items-center justify-center gap-1.5 rounded-lg bg-sem-action-primary px-3 py-2 text-sm font-semibold text-white transition hover:bg-sem-action-primary-hover disabled:opacity-50";
const BTN_SECONDARY =
"inline-flex items-center justify-center gap-1.5 rounded-lg border border-sem-border bg-sem-surface-raised px-3 py-2 text-sm font-medium text-sem-fg transition hover:bg-sem-surface-muted disabled:opacity-50";
const NAME_COLORS = ["#ef4444", "#f97316", "#eab308", "#22c55e", "#14b8a6", "#3b82f6", "#8b5cf6", "#ec4899"];

export default {
name: "RelayHostModerationPage",
components: { MaterialDesignIcon },
props: {
hub: { type: Object, default: null },
initialTab: { type: String, default: "rooms" },
roomFilter: { type: String, default: null },
},
emits: ["back", "refresh"],
data() {
return {
RELAY_HOST_PAGE_HEADER,
RELAY_HOST_PAGE_TABS,
RELAY_HOST_PAGE_TAB,
RELAY_HOST_PAGE_TAB_ACTIVE,
RELAY_HOST_PAGE_TAB_IDLE,
RELAY_HOST_PAGE_BODY,
RELAY_HOST_PAGE_LIST,
RELAY_HOST_PAGE_DETAIL,
RELAY_HOST_ICON_BTN,
RELAY_HOST_LIST_ITEM,
RELAY_HOST_LIST_ITEM_SELECTED,
RELAY_HOST_LIST_ITEM_IDLE,
RELAY_HOST_DETAIL_HEADER,
RELAY_HOST_MESSAGE,
btnPrimary: BTN_PRIMARY,
btnSecondary: BTN_SECONDARY,
tab: "rooms",
isNarrow: false,
mq: null,
roomsLoading: false,
membersLoading: false,
creatingRoom: false,
messagesLoading: false,
showAddRoomForm: false,
rooms: [],
recent: [],
members: [],
roomsSearch: "",
membersSearch: "",
selectedRoom: null,
selectedMember: null,
memberMessages: [],
newRoom: { name: "", topic: "", key: "" },
localIdentityHash: "",
uptimeTick: 0,
uptimeAnchorMs: 0,
uptimeTimer: null,
};
},
computed: {
liveUptimeSeconds() {
void this.uptimeTick;
if (!this.hub?.running) {
return 0;
}
const base = Number(this.hub.uptime_seconds) || 0;
if (!this.uptimeAnchorMs) {
return base;
}
return base + Math.floor((Date.now() - this.uptimeAnchorMs) / 1000);
},
navbarMemberCount() {
if (this.roomFilter) {
if (this.membersLoading) {
return null;
}
return this.members.length;
}
if (this.hub?.clients != null) {
return this.hub.clients;
}
if (!this.membersLoading) {
return this.members.length;
}
return null;
},
filteredRooms() {
const q = this.roomsSearch.trim().toLowerCase();
if (!q) {
return this.rooms;
}
return this.rooms.filter((room) => {
const name = (room.name || "").toLowerCase();
const topic = (room.topic || "").toLowerCase();
return name.includes(q) || topic.includes(q);
});
},
selectedRoomObj() {
if (!this.selectedRoom) {
return null;
}
return this.rooms.find((room) => room.name === this.selectedRoom) || null;
},
pageTitle() {
if (!this.hub?.name) {
return this.$t("relay_chat.host_moderation_title");
}
if (this.roomFilter) {
return this.$t("relay_chat.host_moderation_title_room", {
hub: this.hub.name,
room: this.roomFilter,
});
}
return this.$t("relay_chat.host_moderation_title_hub", { hub: this.hub.name });
},
roomMessages() {
if (!this.selectedRoom) {
return [];
}
return this.recent.filter((m) => m.room === this.selectedRoom);
},
filteredMembers() {
const q = this.membersSearch.trim().toLowerCase();
if (!q) {
return this.members;
}
return this.members.filter((m) => {
const name = (m.name || "").toLowerCase();
const hash = (m.hash || "").toLowerCase();
const rooms = (m.rooms || []).join(" ").toLowerCase();
return name.includes(q) || hash.includes(q) || rooms.includes(q);
});
},
},
watch: {
hub: {
immediate: true,
handler() {
this.uptimeAnchorMs = Date.now();
this.reload();
},
},
initialTab: {
immediate: true,
handler(val) {
this.tab = val === "members" ? "members" : "rooms";
},
},
tab(val) {
if (val === "rooms") {
this.fetchActivity();
} else {
this.fetchMembers();
}
},
},
mounted() {
this.mq = window.matchMedia("(max-width: 1023px)");
this.isNarrow = this.mq.matches;
this.mq.addEventListener("change", this.onMq);
this.tab = this.initialTab === "members" ? "members" : "rooms";
this.ensureLocalIdentity();
this.uptimeAnchorMs = Date.now();
this.uptimeTimer = window.setInterval(() => {
if (this.hub?.running) {
this.uptimeTick += 1;
}
}, 1000);
if (this.hub?.id) {
this.reload();
}
},
beforeUnmount() {
if (this.mq) {
this.mq.removeEventListener("change", this.onMq);
}
if (this.uptimeTimer) {
clearInterval(this.uptimeTimer);
}
},
methods: {
onMq() {
this.isNarrow = this.mq.matches;
},
setTab(next) {
this.tab = next;
if (next === "rooms") {
this.selectedMember = null;
} else {
this.selectedRoom = null;
}
},
reload() {
this.selectedRoom = null;
this.selectedMember = null;
this.memberMessages = [];
this.newRoom = { name: "", topic: "", key: "" };
this.showAddRoomForm = false;
this.roomsSearch = "";
if (this.hub?.running) {
this.fetchMembers();
} else {
this.members = [];
}
if (this.tab === "rooms") {
this.fetchActivity();
}
},
cancelAddRoom() {
this.showAddRoomForm = false;
this.newRoom = { name: "", topic: "", key: "" };
},
async fetchActivity() {
if (!this.hub?.id) {
return;
}
this.roomsLoading = true;
try {
const response = await window.api.get(`/api/v1/rrc/servers/${this.hub.id}/activity`);
this.rooms = response.data?.rooms || [];
this.recent = response.data?.recent || [];
} catch (e) {
ToastUtils.error(e.response?.data?.message || this.$t("relay_chat.action_failed"));
} finally {
this.roomsLoading = false;
}
},
async fetchMembers() {
if (!this.hub?.id) {
return;
}
this.membersLoading = true;
try {
const params = this.roomFilter ? { params: { room: this.roomFilter } } : {};
const response = await window.api.get(`/api/v1/rrc/servers/${this.hub.id}/members`, params);
this.members = response.data?.members || [];
} catch (e) {
ToastUtils.error(e.response?.data?.message || this.$t("relay_chat.action_failed"));
} finally {
this.membersLoading = false;
}
},
async createRoom() {
const name = (this.newRoom.name || "").trim();
if (!name || !this.hub?.id) {
ToastUtils.warning(this.$t("relay_chat.room_required"));
return;
}
this.creatingRoom = true;
try {
await window.api.post(`/api/v1/rrc/servers/${this.hub.id}/rooms`, {
name,
topic: (this.newRoom.topic || "").trim() || undefined,
key: (this.newRoom.key || "").trim() || undefined,
});
this.newRoom = { name: "", topic: "", key: "" };
this.showAddRoomForm = false;
ToastUtils.success(this.$t("relay_chat.host_room_created"));
this.$emit("refresh");
await this.fetchActivity();
} catch (e) {
ToastUtils.error(e.response?.data?.message || this.$t("relay_chat.action_failed"));
} finally {
this.creatingRoom = false;
}
},
async setRoomKey() {
if (!this.hub?.id || !this.selectedRoom) {
return;
}
const entered = await DialogUtils.prompt(
this.$t("relay_chat.host_room_key_prompt", { room: this.selectedRoom }),
"",
{ inputType: "password" }
);
if (entered == null) {
return;
}
const key = String(entered).trim();
if (!key) {
ToastUtils.warning(this.$t("relay_chat.host_room_key_required"));
return;
}
try {
await window.api.put(
`/api/v1/rrc/servers/${this.hub.id}/rooms/${encodeURIComponent(this.selectedRoom)}/key`,
{ key }
);
ToastUtils.success(this.$t("relay_chat.host_room_key_saved"));
this.$emit("refresh");
await this.fetchActivity();
} catch (e) {
ToastUtils.error(e.response?.data?.message || this.$t("relay_chat.action_failed"));
}
},
async clearRoomKey() {
if (!this.hub?.id || !this.selectedRoom) {
return;
}
const confirmed = await DialogUtils.confirm(this.$t("relay_chat.host_clear_room_key_confirm"));
if (!confirmed) {
return;
}
try {
await window.api.put(
`/api/v1/rrc/servers/${this.hub.id}/rooms/${encodeURIComponent(this.selectedRoom)}/key`,
{ key: null }
);
ToastUtils.success(this.$t("relay_chat.host_room_key_cleared"));
this.$emit("refresh");
await this.fetchActivity();
} catch (e) {
ToastUtils.error(e.response?.data?.message || this.$t("relay_chat.action_failed"));
}
},
async deleteRoom(room) {
const confirmed = await DialogUtils.confirm(this.$t("relay_chat.host_delete_room_confirm"));
if (!confirmed || !this.hub?.id) {
return;
}
try {
await window.api.delete(`/api/v1/rrc/servers/${this.hub.id}/rooms/${encodeURIComponent(room)}`);
ToastUtils.success(this.$t("relay_chat.host_room_deleted"));
if (this.selectedRoom === room) {
this.selectedRoom = null;
}
this.$emit("refresh");
await this.fetchActivity();
} catch (e) {
ToastUtils.error(e.response?.data?.message || this.$t("relay_chat.action_failed"));
}
},
selectMember(member) {
this.selectedMember = member;
this.loadMemberMessages();
},
async loadMemberMessages() {
if (!this.selectedMember || !this.hub?.id) {
return;
}
this.messagesLoading = true;
try {
const params = { peer: this.selectedMember.hash, limit: 200 };
if (this.roomFilter) {
params.room = this.roomFilter;
}
const response = await window.api.get(`/api/v1/rrc/servers/${this.hub.id}/messages`, {
params,
});
this.memberMessages = response.data?.messages || [];
} catch {
this.memberMessages = [];
} finally {
this.messagesLoading = false;
}
},
async ensureLocalIdentity() {
if (this.localIdentityHash) {
return;
}
try {
const response = await window.api.get("/api/v1/config");
const hash = response.data?.identity_hash;
if (typeof hash === "string" && hash.trim()) {
this.localIdentityHash = hash.trim().toLowerCase();
}
} catch {
// config may be unavailable in tests
}
},
async resolveModerationRoom(member, action) {
if (this.roomFilter) {
return this.roomFilter;
}
const needsRoom = action === "kick" || action === "room_ban";
if (!needsRoom) {
return null;
}
const rooms = (member.rooms || []).filter((r) => typeof r === "string" && r.trim());
if (rooms.length === 0) {
ToastUtils.warning(this.$t("relay_chat.host_kick_no_room"));
return null;
}
if (rooms.length === 1) {
return rooms[0];
}
const entered = await DialogUtils.prompt(
this.$t("relay_chat.host_kick_pick_room", {
name: member.name,
rooms: rooms.map((r) => "#" + r).join(", "),
})
);
if (!entered) {
return null;
}
const norm = entered.trim().replace(/^#/, "");
const match = rooms.find((r) => r.toLowerCase() === norm.toLowerCase());
if (!match) {
ToastUtils.warning(this.$t("relay_chat.host_kick_room_invalid"));
return null;
}
return match;
},
async moderate(member, action) {
if (!this.hub?.id || !member?.hash) {
return;
}
await this.ensureLocalIdentity();
if (this.localIdentityHash && member.hash.toLowerCase() === this.localIdentityHash) {
ToastUtils.warning(this.$t("relay_chat.host_cannot_moderate_self"));
return;
}
const room = await this.resolveModerationRoom(member, action);
if ((action === "kick" || action === "room_ban") && !room) {
return;
}
const labels = {
kick: this.$t("relay_chat.host_kick_confirm", { name: member.name, room }),
ban: this.$t("relay_chat.host_ban_confirm", { name: member.name }),
room_ban: this.$t("relay_chat.host_room_ban_confirm", {
name: member.name,
room,
}),
};
const confirmed = await DialogUtils.confirm(labels[action] || "");
if (!confirmed) {
return;
}
try {
await window.api.post(`/api/v1/rrc/servers/${this.hub.id}/moderate`, {
action,
peer: member.hash,
room: room || undefined,
});
ToastUtils.success(this.$t("common.success"));
this.$emit("refresh");
await this.fetchMembers();
if (this.selectedMember?.hash === member.hash) {
this.selectedMember = null;
this.memberMessages = [];
}
} catch (e) {
ToastUtils.error(e.response?.data?.message || this.$t("relay_chat.action_failed"));
}
},
colorForHash(hash) {
if (!hash) {
return undefined;
}
let n = 0;
for (let i = 0; i Tff7b72< hash.length; i++) {
Te6edf3n = Ta5d6ff(n + hash.charCodeAt(i)) % NAME_COLORS.length;
}
return NAME_COLORS[n];
},
formatHash(hash) {
if (!hash || hash.length < 16) {
return hash || "";
}
return hash.slice(0, 8) + "..." + hash.slice(-8);
},
formatTime(ts) {
if (!ts) {
return "";
}
return new Date(ts).toLocaleString();
},
formatUptime(seconds) {
if (Te6edf3seconds =Ta5d6ff= null || seconds < 0) {
return "-";
}
let Te6edf3s = Ta5d6ffMath.floor(seconds);
if (s < 60) {
return `${s}s`;
}
if (s < 3600) {
return `${Math.floor(s / 60)}m`;
}
if (s < 86400) {
return `${Math.floor(s / 3600)}h`;
}
if (s < 30 * 86400) {
return `${Math.floor(s / 86400)}d`;
}
const Te6edf3yearSec = Ta5d6ff365 * 86400;
const Te6edf3monthSec = Ta5d6ff30 * 86400;
const Te6edf3years = Ta5d6ffMath.floor(s / yearSec);
s Te6edf3-= Ta5d6ffyears * yearSec;
const Te6edf3months = Ta5d6ffMath.floor(s / monthSec);
s Te6edf3-= Ta5d6ffmonths * monthSec;
const Te6edf3days = Ta5d6ffMath.floor(s / 86400);
const Te6edf3parts = Ta5d6ff[];
if (years) {
parts.push(`${years}y`);
}
if (months) {
parts.push(`${months}mo`);
}
if (days) {
parts.push(`${days}d`);
}
return parts.length ? parts.join(" ") : "0d";
},
copyHash(hash) {
if (!hash) {
return;
}
try {
navigator.clipboard.writeText(hash);
ToastUtils.success(this.$t("relay_chat.hash_copied"));
} catch {
ToastUtils.error(this.$t("common.failed_to_copy"));
}
},
},
};
</scriptTff7b72>

Served by rngit 1.5.2 - Generated in 0.05s